from datetime import datetime, timedelta
import backtrader as bt
from backtrader.utils import logger
import json
import os

from backtrader.utils.datehelper import date2str


class BaseBinanceStrategy(bt.Strategy):
    params = (
        # period for the fast Moving Average
        ('name', "BaseStrategy"),
        ('max_amount', 100),      # 开仓最大金额
        ('max_size', 20),        # 最大仓位
        ('max_trade', 1),       # 小于等于0为无限制
        ('ordtype', bt.Order.ExecTypes[bt.Order.Market]),   # 委托单类型
        ('slippage', 0),        # 滑点
        ('token', ""),
        ("leverage", 1),
        ('enable', False),
        ('debug', False),
    )

    def __init__(self):
        super(BaseBinanceStrategy, self).__init__()
        self.run_count = 0
        # self.symbol = self.data._dataname
        # self.markets = self.broker.fetch_market(symbol=self.symbol)
        self.logger = logger.getLogger(__name__)
        self.alert = bt.utils.Alert(self.p.token, self.p.enable)
        self.dump_cfg = bt.AutoOrderedDict()
        # 交易次数, 统计提交开仓的次数
        self.trade_count = 0

        self.cfg_dir = os.path.join(os.path.expanduser('~'), ".mercury")
        if not os.path.exists(self.cfg_dir):
            os.mkdir(self.cfg_dir)

        self.load()


    def empty(self, value):
        return value <= 10

    def nextstart(self):
        # 加载已经存在的当前挂单
        self.broker.load_opens_order(self)
        params = self.p.__dict__.copy()
        self.logger.info("*" * 77)
        for k, v in params.items():
            if "k" == "token":
                continue
            self.logger.info("参数: @@@@【%15s = %s(%s)】", k, v, type(v).__name__)
        self.logger.info("*" * 77)

    def check_open_amount(self, amount):
        if self.p.max_amount <= 0:
            return False, amount, amount

        if amount > self.p.max_amount:
            return True, self.p.max_amount, amount
        else:
            return False, amount, amount

    def send_msg(self, symbol, action, content, dt):
        _format = self.alert.format()
        params = {
            "name": self.p.name,
            "asset": symbol,
            "action": action,
            "detail": content,
            "date": dt
        }
        self.alert.send(_format.format(**params))

    # def cal_swap_size(self, money, price, leverage):
    #     """
    #     把钱换算成多少张合约
    #     """
    #     # 合约面值
    #     ctVal = float(self.markets["info"]["ctVal"])
    #     return int((money * leverage) / (ctVal * price))
    #
    # def cal_swap_value(self, price, leverage, size):
    #     """
    #     买一张合约的价格
    #     """
    #     ctVal = float(self.markets["info"]["ctVal"])
    #     value = size * ctVal * price
    #     margin = value / leverage
    #     return value, margin

    def allow_trade(self):
        if self.p.max_trade <= 0:
            return True
        return self.trade_count <= self.p.max_trade

    """
    买现货, 现货market买入的size是买多少钱
    """
    def buy_spot(self, data, exectype, size=None, price=None):
        if not self.allow_trade():
            content = "达到交易上限[%s > %s]" % (self.trade_count, self.p.max_trade)
            self.send_msg(data._name, "触发交易上限", content, date2str(datetime.today()))
            raise Exception(f"System auto exit")

        if exectype == bt.Order.Limit and price is None:
            raise Exception(f"market must give the price")
        if self.p.debug:
            cash, value = self.broker.getcash(), self.broker.getvalue(self.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.log("BUY SPOT", data._name, text, bar_dt0)

        if exectype == bt.Order.Limit and self.p.slippage > 0:
            price = price * (1 + self.p.slippage)
        self.trade_count += 1
        return self.buy(data, exectype=exectype, size=size, price=price)

    """
    卖现货
    """
    def sell_spot(self, data, exectype, size=None, price=None):
        if exectype != bt.Order.Market and price is None:
            raise Exception("exec type is not market, please give the price.")
        if self.p.debug:
            cash, value = self.broker.getcash(), self.broker.getvalue(self.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.log("SELL SPOT", data._name, text, bar_dt0)
        if exectype == bt.Order.Limit and self.p.slippage > 0:
            price = price * (1 - self.p.slippage)
        return self.sell(data, exectype=exectype, size=size, price=price)

    """
        止损单
        STOP_LOSS	quantity, stopPrice
        STOP_LOSS_LIMIT quantity, stopPrice, price
        币安交易所不支持stop_loss
        """

    def stop_loss_spot(self, data, exectype, size, stop_price, price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        if exectype not in [bt.Order.StopLimit]:
            raise Exception("exec type is not Stop or StopLimit.")
        if exectype == bt.Order.StopLimit and price is None:
            raise Exception("exec type StopLimit, price is must given.")
        if exectype is None or size is None or stop_price is None or price is None:
            raise Exception("the parameter is not given.")

        if self.p.debug:
            cash, value = self.broker.getcash(), self.broker.getvalue(self.datas)
            money = size * data.close[0]

            text = "Cash: %.2f, Value: %.2f, Type: %s, Price: %.6f, Stop Price: %.6f, Money: %.2f, Size: %.6f" \
                   % (cash, value, bt.Order.ExecTypes[exectype], price if price else 0, stop_price if stop_price else 0, money, size)
            bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
            self.log("STOPLOSS SPOT", data._name, text, bar_dt0)
        return self.sell(data, exectype=exectype, size=size, price=price, params=params)

    # """
    # 合约是多少张, 每张只多少个交易货币是从fetch_market返回的
    # 比特币一张合约值0.01 btc, 以太坊一张合约值0.1 eth
    # """
    # def open_long(self, exectype, size, price):
    #     tdMode, posSide, sz = "isolated", "long", int(size)
    #     return self.buy(exectype=exectype, size=size, price=price, tdMode=tdMode, posSide=posSide, sz=sz)
    #
    # """
    # 设置止盈止损的时候，可平仓量变少，不能强平，必须先取消止盈止损, 或者直接使用市价强平
    # """
    # def close_long(self, exectype, size, price):
    #     tdMode, posSide, sz = "isolated", "long", int(size)
    #     return self.sell(exectype=exectype, size=size, price=price, tdMode=tdMode, posSide=posSide, sz=sz)
    #
    # def open_short(self, exectype, size, price):
    #     tdMode, posSide, sz = "isolated", "short", int(size)
    #     return self.sell(exectype=exectype, size=size, price=price, tdMode=tdMode, posSide=posSide, sz=sz)
    #
    # def close_short(self, exectype, size, price):
    #     tdMode, posSide, sz = "isolated", "short", int(size)
    #     return self.buy(exectype=exectype, size=size, price=price, tdMode=tdMode, posSide=posSide, sz=sz)
    #
    # """
    # 止损单
    # """
    # def stop_loss_swap(self, symbol, size, side, posSide, slTriggerPx, slOrdPx):
    #     """
    #     private_post_trade_order_algo: 策略委托，止盈止损
    #     private_post_trade_cancel_algos: 取消策略委托
    #     private_post_trade_close_position: 按市价平仓
    #     """
    #     exchange = self.broker.store.exchange
    #     params = {
    #         "instId": symbol,
    #         "ordType": "conditional",  # conditional：单向止盈止损, oco：双向止盈止损, trigger：计划委托
    #         "tdMode": "isolated",       # 逐仓
    #         "posSide": posSide,         # 双边持仓指定long还是short
    #         "side": side,                # 操作方向是买还是卖
    #         "sz": size,
    #         "slTriggerPx": slTriggerPx,  # 止损触发价
    #         "slOrdPx": slOrdPx  # 止损委托价, -1就是市价委托
    #     }
    #     _order = exchange.private_post_trade_order_algo(params=params)
    #     if _order["code"] == '0':
    #         self.log('**NOTIFY** ORDER Stop Loss Swap, Side: %s, posSide: %s, Trigger price: %.2f, Order price: %.2f, Size %.2f' %
    #                  (side, posSide, slTriggerPx, slOrdPx, size))
    #         return _order["data"][0]["algoId"]
    #     else:
    #         return None

    """
    市价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """
    def take_profit_spot(self, symbol, size, stop_price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT", side="SELL", amount=size, price=None,
                                        params=params)
            return _order["id"]
        except Exception as e:
            self.log(e)
        return None

    """
    限价止盈单
    STOP_LOSS_LIMIT	timeInForce, quantity, price, stopPrice
    """
    def take_profit_limit_spot(self, symbol, size, stop_price, price):
        params = {
            "type": "spot",
            "stopPrice": stop_price
        }
        try:
            exchange = self.broker.store.exchange
            _order = exchange.create_order(symbol=symbol, type="TAKE_PROFIT_LIMIT", side="SELL", amount=size,
                                            price=price, params=params)
            return _order["id"]
        except Exception as e:
            self.log(e)
        return None

    def fetch_stop_loss_orders(self, symbol):
        """
            "info": {
                "symbol": "FILUSDT",
                "orderId": "1534410584",
                "orderListId": "-1",
                "clientOrderId": "x-R4BD3S82ed0006589bab596ea8af0d",
                "price": "60.00000000", # 委托价格
                "origQty": "1.00000000",    # 委托数量
                "executedQty": "0.00000000",    # 执行数量
                "cummulativeQuoteQty": "0.00000000",    # 累计交易金额
                "status": "NEW",    # 订单状态
                "timeInForce": "GTC",   # 订单时效方式
                "type": "STOP_LOSS_LIMIT",  # 订单类型
                "side": "SELL",     # 订单方向
                "stopPrice": "60.00000000", # 触发价格
                "icebergQty": "0.00000000", # 冰山数量
                "time": "1635754994327",    # 订单时间
                "updateTime": "1635754994327",  # 最后更新时间
                "isWorking": false,             # 订单是否在order book中
                "origQuoteOrderQty": "0.00000000"   # 原始交易金额
            }
        """
        params = {
            "type": "spot"
        }
        try:
            exchange = self.broker.store.exchange
            _order = exchange.fetch_open_orders(symbol=symbol, params=params)
            return _order
        except Exception as e:
            self.log(e)
        return None

    # def cancel_stop_loss(self, symbol, order_id):
    #     try:
    #         exchange = self.broker.store.exchange
    #         params = {
    #             "type": "spot"
    #         }
    #         return exchange.cancel_order(id=int(order_id), symbol=symbol, params=params)
    #     except Exception as e:
    #         self.log(e)
    #     return None

    def log(self, label, symbol, txt, bar_dt=None):
        ''' Logging function fot this strategy'''
        if bar_dt is None:
            bar_dt = datetime.today()
        if isinstance(bar_dt, float) or isinstance(bar_dt, int):
            bar_dt0 = datetime.fromtimestamp(int(bar_dt / 1000))
            if bar_dt0.year <= 1980:
                bar_dt = bt.num2date(bar_dt)
            else:
                bar_dt = bar_dt0
        dt_str = bar_dt.strftime("%Y-%m-%d %H:%M:%S")
        self.logger.info("[**%15s**], [bar: %s], [symbol: %s], [%s]", label, dt_str, symbol, txt)

    def prenext(self):
        '''
        从第一条数据开始, 在miniperiod 阶段中调用, 当满足miniperiod时候就不调用了
        比如ma(3), 前两条调用prenext, 第3条数据的时候调用next
        '''
        self.run_count += 1
        for i, d in enumerate(self.datas):
            if len(d) > 0:
                dtstr = d.datetime.datetime() + timedelta(hours=8)
                text = "Previous Load Data, run count: [%s]" % self.run_count
                self.log("PRENEXT DATA", d._name, text, dtstr)

    def notify_data(self, data, status, *args, **kwargs):
        symbol = data._name
        text = 'Data Status: %s' % data._getstatusname(status)
        self.log("STATUS NOTIFY", symbol, text)

        if data._getstatusname(status) == 'LIVE':
            self.live_data = True
            self.indicator_print(data)
        else:
            self.live_data = False

    def notify_order(self, order):
        symbol = order.data._name
        cash, value = self.broker.getcash(), self.broker.getvalue(self.datas)
        text = "Cash : %.2f, Value: %.2f" % (cash, value)
        self.log("ACCOUNT NOTIFY", symbol, text, order.created.dt)

        if order.status == order.Submitted:
            self.log("ORDER NOTIFY", symbol, "Order Submitted, [Id: %s]" % order.id, order.created.dt)
            return
        if order.status == order.Accepted:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            text = "Order Submitted Completed, [Id: %s, Type: %s, Price: %s, Size: %s, pricelimit %s]" % \
                   (order.id, bt.Order.ExecTypes[order.exectype], order.price, order.size, order.pricelimit)

            self.log("ORDER NOTIFY", symbol, text, order.created.dt)
            return

        if order.status in [order.Expired]:
            self.log("ORDER NOTIFY", symbol, "Order Expired, [Id: %s]" % order.id, order.created.dt)

        elif order.status in [order.Completed]:
            if order.isbuy():
                text = "Order Buy Completed, [Id: %s, Type: %s, Price: %.6f, Size: %s, Comm %s]" % \
                       (order.id, bt.Order.ExecTypes[order.exectype], order.executed.price if order.executed.price else 0, order.executed.size, order.executed.comm)
                self.log("ORDER NOTIFY", symbol, text, order.executed.dt)

            else:  # Sell
                text = "Order Sell Completed, [Id: %s, Type: %s, Price: %.6f, Size: %s, Comm %s]" % \
                       (order.id, bt.Order.ExecTypes[order.exectype], order.executed.price if order.executed.price else 0, order.executed.size, order.executed.comm)
                self.log("ORDER NOTIFY", symbol, text, order.executed.dt)
        elif order.status in [order.Canceled]:
            text = "Order Cancel Completed, [Id: %s, Type: %s, Price: %.6f, Size: %s, pricelimit %s]" % \
                   (order.id, bt.Order.ExecTypes[order.exectype], order.price if order.price else 0, order.size, order.pricelimit)
            self.log("ORDER NOTIFY", symbol, text, order.created.dt)
        elif order.status in [order.Margin]:
            self.log("ORDER NOTIFY", symbol, "Order Failed, not enough cash. [Id: %s]" % order.id, order.created.dt)

    def dump_status(self):
        pass

    def dump(self):
        self.dump_status()
        cfg_path = os.path.join(self.cfg_dir, f"{self.p.name}.json")

        with open(cfg_path, 'w') as f:
            json.dump(self.dump_cfg, f, indent=4)

    def load(self):
        cfg_path = os.path.join(self.cfg_dir, f"{self.p.name}.json")
        if os.path.exists(cfg_path):
            with open(cfg_path, 'r') as f:
                data = json.load(f)
            for k, v in data.items():
                self.dump_cfg[k] = v

    def data_print(self, data):
        bar_dt0 = data.datetime.datetime() + timedelta(hours=8)
        d = data
        open0, high0, low0, close0, vol0, amount0 = d.open[0], d.high[0], d.low[0], d.close[0], d.volume[0], d.amount[0]
        text = "Live: %s, O: %.4f, H: %.4f, L: %.4f, C: %.4f, V: %.4f, A: %.4f" % \
               (self.live_data, open0, high0, low0, close0, vol0, amount0)
        self.log("DATA NOTIFY", data._name, text, bar_dt0)